/*
 * Galaxium Messenger
 * Copyright (C) 2007 Paul Burton <paulburton89@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

using Anculus.Core;

namespace Galaxium.Protocol.Msn
{
	public delegate void AckHandler (P2PMessage ack);
	
	public static class MsnP2PUtility
	{
		struct P2PAppInfo
		{
			public Type appType;
			public IMsnP2PApplication app;
			public Guid eufGuid;
			public uint appId;
		}
		
		static List<P2PAppInfo> _apps = new List<P2PAppInfo> ();
		static List<MsnP2PSession> _sessions = new List<MsnP2PSession> ();
		
		static Dictionary<uint, MemoryStream> _messageStreams = new Dictionary<uint, MemoryStream> ();
		
		static Dictionary<uint, AckHandler> _ackHandlers = new Dictionary<uint, AckHandler> ();
		
		// Returns all non-session P2P applications
		public static IEnumerable<IMsnP2PApplication> Applications
		{
			get
			{
				foreach (P2PAppInfo info in _apps)
					if (info.app != null)
						yield return info.app;
			}
		}
		
		public static IEnumerable<MsnP2PSession> Sessions
		{
			get { return _sessions; }
		}
		
		static MsnP2PUtility ()
		{
			try
			{
				AutoAddAssembly (Assembly.GetExecutingAssembly ());
			}
			catch (Exception ex)
			{
				Log.Error (ex, "Exception initializing MsnP2PUtility");
			}
		}
		
		public static void AutoAddAssembly (Assembly asm)
		{
			foreach (Type t in asm.GetTypes ())
			{
				if (t.GetCustomAttributes (typeof (MsnP2PApplicationAttribute), false).Length > 0)
					AddType (t);
			}
		}
		
		public static void AddType (Type t)
		{
			foreach (MsnP2PApplicationAttribute att in t.GetCustomAttributes (typeof (MsnP2PApplicationAttribute), false))
			{
				P2PAppInfo appInfo = new P2PAppInfo ();
				appInfo.appType = t;
				appInfo.appId = att.AppID;
				appInfo.eufGuid = att.EufGuid;
				
				if (t.GetInterface ("IMsnP2PSessionApplication") == null)
					appInfo.app = Activator.CreateInstance (t) as IMsnP2PApplication;

				_apps.Add (appInfo);
			}
		}
		
		public static void Add (IMsnP2PSessionApplication app)
		{
			MsnP2PSession session = new MsnP2PSession (app);
			session.Closed += SessionClosed;
			
			_sessions.Add (session);
		}
		
		public static MsnP2PSession FindSession (P2PMessage msg)
		{
			uint sessionID = msg.Header.SessionID;
			
			if ((sessionID == 0) && (msg.SLPMessage != null))
			{
				if (msg.SLPMessage.MIMEBody.ContainsKey ("SessionID"))
				{
					if (!uint.TryParse (msg.SLPMessage.MIMEBody["SessionID"].Value, out sessionID))
					{
						Log.Warn ("Unable to parse SLP message SessionID");
						sessionID = 0;
					}
				}
				
				if (sessionID == 0)
				{
					// We don't get a session ID in BYE requests
					// so we need to find the session by its call ID
					
					foreach (MsnP2PSession session in _sessions)
					{
						if (session.Invite.CallID == msg.SLPMessage.CallID)
							return session;
					}
				}
			}
			
			// Sometimes we only have a message ID to find the session with...
			// e.g. the waiting (flag 4) messages wlm sends sometimes
			if ((sessionID == 0) && (msg.Header.MessageID != 0))
			{
				foreach (MsnP2PSession session in _sessions)
				{
					uint expected = session.RemoteID + 1;
					
					if (expected == session.RemoteBaseID)
						expected++;
					
					//Log.Debug ("MessageID {0}, expected {1}", msg.Header.MessageID, expected);
					
					if (msg.Header.MessageID == expected)
						return session;
				}
			}
			
			if (sessionID == 0)
				return null;
			
			foreach (MsnP2PSession session in _sessions)
			{
				if (session.SessionID == sessionID)
					return session;
			}
			
			return null;
		}
		
		public static IMsnP2PApplication FindApplication (P2PMessage msg)
		{
			MsnP2PSession session = FindSession (msg);
			
			if (session != null)
				return session.Application;
			
			if (msg.Footer != 0)
			{
				// If the message is intended for a non-session based P2P application
				// (like P2P based ink) then we find the application by the appID in the footer
				//TODO: should we also check the sessionID (which seems to be constant?)
				
				foreach (P2PAppInfo info in _apps)
				{
					if (info.app == null)
						continue;
					
					if (info.appId == msg.Footer)
						return info.app;
				}
			}
			
			return null;
		}
		
		public static void ProcessMessage (IMsnP2PBridge bridge, P2PMessage msg)
		{
			if (HandleSplitMessage (ref msg))
				return;
			
			if ((msg.SLPMessage != null) && (msg.SLPMessage.To != msg.Session.Account))
			{
				Log.Debug ("Received P2P message intended for {0}, not us\n{1}", msg.SLPMessage.To.UniqueIdentifier, msg);
				
				if (msg.SLPMessage.From.Local)
					Log.Error ("We received a message from ourselves?");
				else
					SendStatus (bridge, msg, msg.SLPMessage.From, 404, "Not Found");
				
				return;
			}
			
			if (msg.Header.AckUID != 0)
			{
				// This is an ack
				
				//TODO: is this the best way to detect acks?
				// there's the Ack flag, but thats not set
				// for the bye ack
				
				if (_ackHandlers.ContainsKey (msg.Header.AckUID))
				{
					// An AckHandler has been registered for this ack
					
					_ackHandlers[msg.Header.AckUID] (msg);
					_ackHandlers.Remove (msg.Header.AckUID);
				}
				else
					Log.Debug ("No AckHandler for ack {0}", msg.Header.AckUID);
				
				return;
			}
			
			MsnP2PSession session = FindSession (msg);
			
			if (session != null)
			{
				if (!session.ProcessMessage (bridge, msg))
				{
					Log.Warn ("P2PSession {0} could not process message\n{1}", session.SessionID, msg);
					SendStatus (bridge, msg, session.Remote, 500, "Internal Error");
				}
				
				return;
			}
			
			IMsnP2PApplication app = FindApplication (msg);
			
			if (app != null)
			{
				if (!app.ProcessMessage (bridge, msg))
				{
					Log.Warn ("P2PApp could not process message\n{0}", msg);
					SendStatus (bridge, msg, app.Remote, 500, "Internal Error");
				}
				
				return;
			}
			
			if (msg.SLPMessage is SLPRequestMessage)
			{
				SLPRequestMessage req = msg.SLPMessage as SLPRequestMessage;
				
				if ((req.Method == "INVITE") && (req.ContentType == "application/x-msnmsgr-sessionreqbody"))
				{
					// Start a new session
					
					session = new MsnP2PSession (msg);
					session.Closed += SessionClosed;
					
					lock (_sessions)
						_sessions.Add (session);
					
					return;
				}
			}
			
			if ((msg.Header.Flags & P2PHeaderFlag.Waiting) == P2PHeaderFlag.Waiting)
			{
				Log.Debug ("Received P2P waiting message");
				return;
			}
			
			// Nothing wants this message, but we will ack it anyway (if it's
			// not an ACK itself) to (hopefully) keep the remote client happy
			
			if (msg.Header.AckUID == 0)
				bridge.Send (msg.CreateAck ());
			
			if (msg.SLPMessage != null)
				return;
			
			Log.Warn ("Unhandled P2P message!\n{0}", msg);
		}
		
		static void SessionClosed (object sender, EventArgs args)
		{
			MsnP2PSession session = sender as MsnP2PSession;
			
			Log.Debug ("P2PSession {0} closed, removing", session.SessionID);
			
			lock (_sessions)
				_sessions.Remove (session);
			
			session.Dispose ();
		}
		
		static void SendStatus (IMsnP2PBridge bridge, P2PMessage msg, IMsnEntity dest, int code, string phrase)
		{
			SLPMessage slp = new SLPStatusMessage (dest, code, phrase);
			
			if (msg.SLPMessage != null)
			{
				slp.Branch = msg.SLPMessage.Branch;
				slp.CallID = msg.SLPMessage.CallID;
				slp.From = msg.SLPMessage.To;
				slp.ContentType = msg.SLPMessage.ContentType;
			}
			else
				slp.ContentType = "null";
				
			P2PMessage response = new P2PMessage (msg.Session);
			response.SLPMessage = slp;
				
			bridge.Send (msg);
		}
		
		public static void RegisterAckHandler (uint ackID, AckHandler handler)
		{
			_ackHandlers.Add (ackID, handler);
		}
		
		public static P2PMessage[] SplitMessage (P2PMessage msg, uint maxChunkSize)
		{
			if (msg.Payload.Length <= maxChunkSize)
				return new P2PMessage[] { msg };
			
			List<P2PMessage> chunks = new List<P2PMessage> ();
			ulong offset = 0;
			
			while (offset < (ulong)msg.Payload.Length)
			{
				P2PMessage chunk = new P2PMessage (msg);
				
				uint chunkSize = Math.Min (maxChunkSize, (uint)((ulong)msg.Payload.Length - offset));
				byte[] chunkPayload = new byte [chunkSize];
				
				Array.Copy (msg.Payload, (long)offset, chunkPayload, 0, chunkSize);
				
				chunk.Header.ChunkOffset = offset;
				chunk.Header.ChunkSize = chunkSize;
				chunk.Header.TotalSize = (ulong)msg.Payload.Length;
				
				chunk.Payload = chunkPayload;
				
				chunks.Add (chunk);
				
				offset += chunkSize;
			}
			
			return chunks.ToArray ();
		}
		
		static bool HandleSplitMessage (ref P2PMessage msg)
		{
			// If this is not a split message, return false
			if (((msg.Header.Flags & P2PHeaderFlag.Data) == P2PHeaderFlag.Data) || (msg.Payload.Length == 0) || (msg.Header.ChunkSize == msg.Header.TotalSize))
				return false;
			
			Log.Debug ("Caught split P2P message");
			
			if (!_messageStreams.ContainsKey (msg.Header.MessageID))
				_messageStreams.Add (msg.Header.MessageID, new MemoryStream ());
				
			_messageStreams[msg.Header.MessageID].Write (msg.Payload, 0, msg.Payload.Length);
			
			if ((msg.Header.ChunkOffset + msg.Header.ChunkSize) >= msg.Header.TotalSize)
			{
				// We have the whole message
				
				msg.Header.ChunkOffset = 0;
				msg.Header.ChunkSize = (uint)msg.Header.TotalSize;
				
				msg.Payload = _messageStreams[msg.Header.MessageID].ToArray ();
				
				_messageStreams[msg.Header.MessageID].Close ();
				_messageStreams.Remove (msg.Header.MessageID);
				
				return false;
			}
			
			return true;
		}
		
		internal static Guid GetEufGuid (IMsnP2PApplication app)
		{
			foreach (P2PAppInfo info in _apps)
			{
				if (info.appType == app.GetType ())
					return info.eufGuid;
			}
			
			return new Guid ();
		}
		
		internal static uint GetAppID (IMsnP2PApplication app)
		{
			foreach (P2PAppInfo info in _apps)
			{
				if (info.appType == app.GetType ())
					return info.appId;
			}
			
			return 0;
		}
		
		internal static Type GetApp (Guid eufGuid)
		{
			foreach (P2PAppInfo info in _apps)
			{
				if (info.eufGuid == eufGuid)
					return info.appType;
			}
			
			return null;
		}
		
		internal static IMsnP2PApplication GetApp (uint appID)
		{
			foreach (P2PAppInfo info in _apps)
			{
				if (info.app == null)
					continue;
				
				if (info.appId == appID)
					return info.app;
			}
			
			return null;
		}
		
		internal static IMsnP2PApplication GetAppInstance (Type appType)
		{
			foreach (P2PAppInfo info in _apps)
			{
				if (info.appType != appType)
					continue;
				
				if (info.app != null)
					return info.app;
				
				return null;
			}
			
			return null;
		}
	}
}
